#!/usr/bin/env python3
# 指定此脚本使用 Python 3 运行。这是用于类 Unix 系统的 shebang 行。

# -*- coding: utf-8 -*-
# 设置源文件编码为 UTF-8，确保正确处理非 ASCII 字符（如中文）。

"""
SiliconFlow 语音转写 CLI
- 支持单文件或目录批量上传
- 可选择使用 ffmpeg 预处理为 16kHz 单声道 WAV 格式
- 输出到同名 .txt 文件（或指定输出目录/文件）
用法示例：
  python transcribe_siliconflow.py audio.wav
  python transcribe_siliconflow.py ./audios --resample --out ./texts
  python transcribe_siliconflow.py a.mp3 --key sk-xxx --model FunAudioLLM/SenseVoiceSmall
"""
# 文档字符串：描述脚本的功能、特性及使用示例。

import argparse
# 导入 argparse 模块，用于处理命令行参数。

import os
# 导入 os 模块，用于与操作系统交互，例如访问环境变量或文件路径。

import sys
# 导入 sys 模块，用于系统相关操作，如退出程序或访问标准错误输出。

import shutil
# 导入 shutil 模块，用于高级文件操作，例如检查 ffmpeg 是否存在。

import tempfile
# 导入 tempfile 模块，用于创建临时文件或目录，用于 ffmpeg 预处理。

import subprocess
# 导入 subprocess 模块，用于在 Python 中运行外部命令，如 ffmpeg。

from pathlib import Path
# 从 pathlib 导入 Path 类，用于现代化的、面向对象的文件路径操作。

from typing import Iterable, List, Optional
# 导入类型提示（Iterable、List、Optional），用于提高代码可读性和静态类型检查。

import requests
# 导入 requests 库，用于向 SiliconFlow API 发送 HTTP 请求。

# from voice-record import start_recording
from voice_record import start_recording
API_BASE = "https://api.siliconflow.cn"
# 定义 SiliconFlow API 的基础 URL。

ENDPOINT = "/v1/audio/transcriptions"
# 定义语音转写的 API 端点。

DEFAULT_MODEL = "FunAudioLLM/SenseVoiceSmall"
# 设置默认的转写模型。

AUDIO_EXTS = {".wav", ".mp3", ".m4a", ".flac", ".ogg", ".aac", ".wma", ".amr", ".webm", ".mp4"}
# 定义支持的音频文件扩展名集合。
WAV_PATH = "./tmp/chat.wav"
def has_ffmpeg() -> bool:
    # 定义函数，检查系统是否安装了 ffmpeg，返回布尔值。
    return shutil.which("ffmpeg") is not None
    # 使用 shutil.which 检查系统 PATH 中是否存在 ffmpeg，返回 True（存在）或 False（不存在）。

def collect_audio_files(p: Path) -> List[Path]:
    # 定义函数，收集指定路径中的音频文件。接受 Path 对象，返回 Path 对象的列表。
    if p.is_file():
        # 检查输入路径是否为单个文件。
        return [p]
        # 如果是文件，返回包含该文件的列表。
    if p.is_dir():
        # 检查输入路径是否为目录。
        files = [x for x in p.rglob("*") if x.suffix.lower() in AUDIO_EXTS and x.is_file()]
        # 使用列表推导式，递归查找目录中扩展名在 AUDIO_EXTS 中的所有文件。
        files.sort()
        # 对文件列表按名称排序，确保处理顺序一致。
        return files
        # 返回排序后的音频文件列表。
    raise FileNotFoundError(f"未找到路径：{p}")
    # 如果路径既不是文件也不是目录，抛出文件未找到错误。

def ffmpeg_resample_to_wav16k_mono(src: Path, tmpdir: Path) -> Path:
    # 定义函数，使用 ffmpeg 将音频文件转为 16kHz 单声道 WAV 格式。返回转换后的临时文件路径。
    """使用 ffmpeg 转 16kHz/mono WAV；返回转换后的临时文件路径。"""
    # 文档字符串，描述函数用途。
    dst = tmpdir / (src.stem + "_16k_mono.wav")
    # 在临时目录中创建目标文件路径，使用源文件名加 "_16k_mono.wav" 后缀。
    cmd = [
        "ffmpeg", "-y",  # ffmpeg 命令，-y 表示覆盖输出文件不提示。
        "-i", str(src),  # 指定输入文件路径。
        "-ac", "1",      # 设置音频通道为 1（单声道）。
        "-ar", "16000",  # 设置音频采样率为 16kHz。
        str(dst),        # 指定输出文件路径。
    ]
    try:
        subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
        # 运行 ffmpeg 命令，抑制输出，如果失败则抛出异常。
    except FileNotFoundError:
        raise RuntimeError("未检测到 ffmpeg，请安装后再使用 --resample")
        # 如果未找到 ffmpeg，抛出运行时错误。
    except subprocess.CalledProcessError as e:
        raise RuntimeError(f"ffmpeg 转码失败：{src}") from e
        # 如果 ffmpeg 转码失败，抛出错误并包含源文件路径。
    return dst
    # 返回转换后的 WAV 文件路径。

def transcribe_file(
    filepath: Path,
    api_key: str,
    model: str = DEFAULT_MODEL,
    base_url: str = API_BASE,
    timeout: int = 300,
) -> str:
    # 定义函数，使用 SiliconFlow API 转写单个音频文件，返回转写文本。
    url = base_url.rstrip("/") + ENDPOINT
    # 构建完整的 API URL，去掉基础 URL 末尾的斜杠并追加端点。
    headers = {"Authorization": f"Bearer {api_key}"}
    # 设置 HTTP 请求头，包含 API 密钥用于身份验证。
    with open(filepath, "rb") as f:
        # 以二进制只读模式打开音频文件。
        files = {"file": (filepath.name, f, "application/octet-stream")}
        # 准备文件上传的 multipart 表单数据，包含文件名和文件内容。
        data = {"model": model}
        # 指定请求中使用的模型。
        resp = requests.post(url, headers=headers, files=files, data=data, timeout=timeout)
        # 向 API 发送 POST 请求，包含文件、模型和超时设置。
    try:
        resp.raise_for_status()
        # 检查 HTTP 请求是否成功，如果失败则抛出异常。
    except requests.HTTPError as e:
        # 捕获 HTTP 错误（如 400、500 状态码）。
        msg = f"HTTP {resp.status_code} - {resp.text[:500]}"
        # 创建错误消息，包含状态码和响应文本的前 500 字符。
        raise RuntimeError(f"转写失败：{msg}") from e
        # 抛出自定义错误，包含 API 响应详情。
    try:
        j = resp.json()
        # 尝试将响应解析为 JSON 格式。
    except Exception:
        raise RuntimeError(f"返回非 JSON：{resp.text[:500]}")
        # 如果响应不是有效 JSON，抛出错误。
    if "text" not in j:
        raise RuntimeError(f"返回缺少 text 字段：{j}")
        # 如果 JSON 响应缺少 "text" 字段，抛出错误。
    return j["text"]
    # 返回 JSON 响应中的转写文本。

def ensure_out_path(in_path: Path, out: Optional[Path], is_batch: bool) -> Path:
    # 定义函数，确定转写结果的输出文件路径。
    if out is None:
        # 如果未指定输出路径，使用输入文件的目录并将扩展名改为 .txt。
        return in_path.with_suffix(".txt")
        # 将输入文件的扩展名替换为 .txt。
    if is_batch:
        # 如果是批量处理模式：
        if out.suffix:  # 检查输出路径是否带有扩展名（可能是文件）。
            out_dir = out.parent
            # 如果是文件路径，使用其父目录作为输出目录。
        else:
            out_dir = out
            # 如果是目录路径，直接使用该目录。
        out_dir.mkdir(parents=True, exist_ok=True)
        # 创建输出目录（如果不存在）。
        return out_dir / (in_path.stem + ".txt")
        # 返回输出目录中以输入文件名命名的 .txt 文件路径。
    else:
        # 如果是单文件处理：
        if out.exists() and out.is_dir():
            return out / (in_path.stem + ".txt")
            # 如果输出路径是目录，在该目录中保存以输入文件名命名的 .txt 文件。
        if not out.suffix:
            # 如果输出路径没有扩展名，视为目录。
            out.mkdir(parents=True, exist_ok=True)
            # 创建目录（如果不存在）。
            return out / (in_path.stem + ".txt")
            # 在该目录中保存以输入文件名命名的 .txt 文件。
        out.parent.mkdir(parents=True, exist_ok=True)
        # 确保输出文件的父目录存在。
        return out
        # 返回指定的输出文件路径。

def get_speech_input():
    # 定义主函数，包含 CLI 的核心逻辑。
    p = argparse.ArgumentParser(description="上传音频到硅基流动 API，返回识别文本")
    # 创建 ArgumentParser 对象，设置 CLI 的描述信息。
    # p.add_argument("path", type=str, help="音频文件或目录路径")
    # （已注释）定义一个必需的路径参数，用于输入文件或目录。
    p.add_argument("--key", type=str, default=None, help="API Key；不填则读 SILICONFLOW_API_KEY 环境变量")
    # 添加可选的 --key 参数，用于指定 API 密钥，默认为 None。
    p.add_argument("--model", type=str, default=DEFAULT_MODEL, help=f"模型名（默认：{DEFAULT_MODEL}）")
    # 添加可选的 --model 参数，指定转写模型，默认为 DEFAULT_MODEL。
    p.add_argument("--out", type=str, default=None, help="输出路径（文件或目录）。默认输出到同名 .txt")
    # 添加可选的 --out 参数，指定输出文件或目录，默认为 None。
    p.add_argument("--resample", action="store_true", help="调用 ffmpeg 预处理为 16kHz/mono WAV（推荐）")
    # 添加 --resample 标志，启用 ffmpeg 预处理。
    p.add_argument("--print", dest="do_print", action="store_true", help="终端打印识别结果")
    # 添加 --print 标志（存储为 do_print），用于在终端打印转写结果。
    p.add_argument("--base-url", type=str, default=API_BASE, help="API Base URL（一般不需要改）")
    # 添加可选的 --base-url 参数，指定 API 基础 URL，默认为 API_BASE。
    p.add_argument("--timeout", type=int, default=300, help="HTTP 超时时间（秒）")
    # 添加可选的 --timeout 参数，指定 HTTP 请求超时时间，默认为 300 秒。
    args = p.parse_args()
    # 解析命令行参数，存储在 args 对象中。

    api_key = args.key or os.getenv("SILICONFLOW_API_KEY")
    # 从 --key 参数或环境变量 SILICONFLOW_API_KEY 获取 API 密钥。
    if not api_key:
        print("缺少 API Key：请使用 --key 或设置环境变量 SILICONFLOW_API_KEY", file=sys.stderr)
        sys.exit(2)
        # 如果未提供 API 密钥，打印错误并以退出码 2 退出。
    start_recording(WAV_PATH)
    in_path = Path(WAV_PATH).expanduser().resolve()
    # 硬编码输入路径为 "./test.wav"，并解析为绝对路径。
    files = collect_audio_files(in_path)
    # 使用 collect_audio_files 函数收集输入路径中的音频文件。
    if not files:
        print("未在该路径下发现可识别的音频文件", file=sys.stderr)
        sys.exit(1)
        # 如果未找到音频文件，打印错误并以退出码 1 退出。

    is_batch = len(files) > 1 or in_path.is_dir()
    # 判断是否为批量处理（文件数大于 1 或输入路径为目录）。
    out_arg = Path(args.out).expanduser().resolve() if args.out else None
    # 如果提供了 --out 参数，解析为绝对路径，否则设置为 None。

    if args.resample and not has_ffmpeg():
        print("提示：未检测到 ffmpeg，将跳过预处理。可取消 --resample 或安装 ffmpeg。", file=sys.stderr)
        args.resample = False
        # 如果启用了 --resample 但未安装 ffmpeg，打印警告并禁用预处理。

    ok, fail = 0, 0
    # 初始化成功和失败计数器。
    with tempfile.TemporaryDirectory() as td:
        # 创建临时目录，用于存储预处理的音频文件，结束后自动删除。
        tmpdir = Path(td)
        # 将临时目录路径转换为 Path 对象。
        for src in files:
            # 遍历音频文件列表。
            try:
                use_path = src
                # 默认使用原始文件路径。
                if args.resample:
                    use_path = ffmpeg_resample_to_wav16k_mono(src, tmpdir)
                    # 如果启用预处理，使用 ffmpeg 转换文件为 16kHz 单声道 WAV。
                text = transcribe_file(
                    use_path, api_key=api_key, model=args.model, base_url=args.base_url, timeout=args.timeout
                )
                # 使用 API 转写音频文件（原始或预处理后的）。
                out_path = ensure_out_path(src, out_arg, is_batch)
                # 确定转写结果的输出路径。
                out_path.write_text(text, encoding="utf-8")
                # 将转写文本以 UTF-8 编码写入输出文件。
                if args.do_print:
                    print(f"\n=== {src.name} ===\n{text}\n")
                    # 如果指定了 --print，在终端打印文件名和转写结果。
                else:
                    print(f"[OK] {src}  ->  {out_path}")
                    # 否则，打印成功信息，显示输入和输出路径。
                ok += 1
                # 成功计数器加 1。
            except Exception as e:
                print(f"[FAIL] {src}  ->  {e}", file=sys.stderr)
                # 如果处理失败，打印错误信息到标准错误输出。
                fail += 1
                # 失败计数器加 1。

    if fail:
        print(f"\n完成：成功 {ok}，失败 {fail}", file=sys.stderr)
        sys.exit(1)
        # 如果有失败，打印总结信息并以退出码 1 退出。
    else:
        print(f"\n完成：成功 {ok}，失败 {fail}")
        # 如果没有失败，打印总结信息并正常退出。
    return text
if __name__ == "__main__":
    get_speech_input()
    # 如果脚本作为主程序运行，调用 main 函数。